package com.castrianni.vcs;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;

import org.jetbrains.annotations.Nullable;
import org.jgraph.JGraph;
import org.jgraph.event.GraphSelectionEvent;
import org.jgraph.event.GraphSelectionListener;
import org.jgraph.graph.CellView;
import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.DefaultGraphModel;
import org.jgraph.graph.DefaultPort;
import org.jgraph.graph.Edge;
import org.jgraph.graph.GraphCell;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.ActionPopupMenu;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diff.DiffManager;
import com.intellij.openapi.diff.DiffTool;
import com.intellij.openapi.diff.SimpleDiffRequest;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.history.VcsHistoryProvider;
import com.intellij.openapi.vcs.ui.ReplaceFileConfirmationDialog;
import com.intellij.openapi.vcs.vfs.VcsFileSystem;
import com.intellij.openapi.vcs.vfs.VcsVirtualFile;
import com.intellij.openapi.vfs.VirtualFile;
import info.clearthought.layout.TableLayout;

public abstract class VcsRevisionGraph extends DialogWrapper implements GraphSelectionListener {
  public static enum CONNECT_TYPE {NO,HALF,YES}

  public static final int CELL_SPACING = 15;
  public static final int MIN_DIALOG_WIDTH = 600;
  public static final int MIN_DIALOG_HEIGHT = 600;
  public static final String DATE_TIME_FORMAT = "MMM dd, yyyy hh:mm:ss a";
  public static final String TIME_FORMAT = "hh:mm:ss a";
  public static final float[] DASH_PATTERN = new float[] {4,4};

  public static final SimpleDateFormat _dateTimeFormat = new SimpleDateFormat(DATE_TIME_FORMAT);
  public static final SimpleDateFormat _timeFormat = new SimpleDateFormat(TIME_FORMAT);

  protected FilePath _filePath = null;
  protected AbstractVcs _vcs = null;
  protected VcsHistoryProvider _historyProvider = null;
  protected Project _project = null;
  protected JGraph _g = null;
  protected DefaultGraphModel _gm = null;
  protected List<VcsRevisionGraphRevision> _revisions = null;
  protected JTextField _revisionTF = null;
  protected JTextField _authorTF = null;
  protected JTextField _dateTF = null;
  protected JTextArea _messageTA = null;
  protected ActionPopupMenu _viewPopup = null;
  protected ActionPopupMenu _fileRevisionPopup = null;
  protected ActionPopupMenu _branchRevisionPopup = null;
  protected boolean _showBranchFilter = false;
  protected List<String> _branchFilter = null;
  protected boolean _showRevisionFilter = false;
  protected boolean _afterDateTimeFilter = false;
  protected boolean _beforeDateTimeFilter = false;
  protected String _afterDateTime = null;
  protected String _beforeDateTime = null;
  protected RevisionStringComparator _revCompare = null;
  protected VcsFileRevisionDateComparator _revDateCompare = null;
  protected VcsRevisionGraphCellViewFactory _cellViewFactory = null;
  protected LinkedHashMap<String,VcsBranch> _branchMap = null;
  protected LinkedHashMap<VcsBranch,ArrayList<VcsRevisionGraphRevision>> _revMap = null;
  protected LinkedHashMap<VcsBranch,ArrayList<VcsRevisionGraphRevision>> _filteredRevMap = null;
  protected HashMap<String,DefaultGraphCell> _cellMap = null;
  protected CompareAction _ca = null;
  protected RefreshAction _ra = null;
  protected GetRevisionAction _gra = null;
  protected ZoomInAction _zia = null;
  protected ZoomOutAction _zoa = null;
  protected ZoomDefaultAction _zda = null;
  protected DateFilterAction _dfa = null;
  protected HideSelectedBranchAction _hsba = null;
  protected ShowAllBranchesAction _saba = null;
  protected VcsRevisionGraphRevision _selRevCell = null;

  public VcsRevisionGraph(Project project,FilePath filePath,AbstractVcs vcs,VcsHistoryProvider historyProvider)
  {
    super(project,false);
    setTitle("Revision Graph: " + filePath.getName());
    setModal(false);
    _filePath = filePath;
    _project = project;
    _vcs = vcs;
    _historyProvider = historyProvider;
  }

  protected abstract String getAllRevisions();

  protected void init()
  {
    _revCompare = new RevisionStringComparator();
    _revDateCompare = new VcsFileRevisionDateComparator();
    _gm = new DefaultGraphModel();
    _g = new JGraph(_gm);
    _cellViewFactory = createCellViewFactory();
    _g.getGraphLayoutCache().setFactory(_cellViewFactory);
    _g.setHandleSize(0);
    _g.setMoveable(false);
    VcsRevisionGraphProjectComponent rgpc = _project.getComponent(VcsRevisionGraphProjectComponent.class);
    _showBranchFilter = rgpc.is_showBranchFilter();
    _branchFilter = rgpc.getBranchFilter();
    if(_branchFilter.isEmpty())
      _showBranchFilter = false;
    _showRevisionFilter = rgpc.is_showRevisionFilter();
    _afterDateTimeFilter = rgpc.is_afterDateTimeFilter();
    _beforeDateTimeFilter = rgpc.is_beforeDateTimeFilter();
    _afterDateTime = rgpc.get_afterDateTime();
    _beforeDateTime = rgpc.get_beforeDateTime();
    String currentRev = getAllRevisions();
    createGraph();
    layoutGraph();
    setCancelButtonText("Close");
    super.init();
    setCurrentRevision(currentRev);
  }

  protected void filterRevisions()
  {
    //Apply branch filters
    _filteredRevMap = new LinkedHashMap<VcsBranch,ArrayList<VcsRevisionGraphRevision>>();
    for(VcsBranch branch : _revMap.keySet()) {
      if(!_branchFilter.isEmpty()) {
        boolean contains = _branchFilter.contains(branch.getName());
        if(_showBranchFilter ? !contains : contains)
          continue;
      }
      _filteredRevMap.put(branch,_revMap.get(branch));
    }
    //Apply revision date filters
    for(VcsBranch branch : _filteredRevMap.keySet()) {
      ArrayList<VcsRevisionGraphRevision> revsOnBranch = _filteredRevMap.get(branch);
      ArrayList<VcsRevisionGraphRevision> filteredRevsOnBranch = new ArrayList<VcsRevisionGraphRevision>();
      for(VcsRevisionGraphRevision rev : revsOnBranch) {
        boolean passesFilter = true;
        if(_afterDateTimeFilter) {
          Date afterDateTime = null;
          try {
            afterDateTime = _dateTimeFormat.parse(_afterDateTime);
          } catch(Exception e) {
            //Ignore
          }
          if((afterDateTime != null) && (afterDateTime.compareTo(rev.getRevisionDate()) > 0))
            passesFilter = false;
        }
        if(_beforeDateTimeFilter) {
          Date beforeDateTime = null;
          try {
            beforeDateTime = _dateTimeFormat.parse(_beforeDateTime);
          } catch(Exception e) {
            //Ignore
          }
          if((beforeDateTime != null) && (beforeDateTime.compareTo(rev.getRevisionDate()) < 0))
            passesFilter = false;
        }
        if((_afterDateTimeFilter) || (_beforeDateTimeFilter)) {
          if(_showRevisionFilter ? !passesFilter : passesFilter)
            continue;
        }
        filteredRevsOnBranch.add(rev);
      }
      _filteredRevMap.put(branch,filteredRevsOnBranch);
    }
  }

  protected VcsRevisionGraphCellViewFactory createCellViewFactory()
  {
    return(new VcsRevisionGraphCellViewFactory(_project));
  }

  @Nullable
  protected JComponent createCenterPanel()
  {
    _g.addGraphSelectionListener(this);
    _g.addMouseListener(new PopupListener());
    JScrollPane sp = new JScrollPane(_g);
    return(sp);
  }

  protected Action[] createActions()
  {
    return(new Action[] {getCancelAction()});
  }

  protected JComponent createNorthPanel()
  {
    DefaultActionGroup toolBarActions = createToolBarActions();
    ActionManager am = ActionManager.getInstance();
    ActionToolbar actionToolbar = am.createActionToolbar(ActionPlaces.UNKNOWN,toolBarActions,true);
    DefaultActionGroup fileRevisionActions = createFileRevisionActions();
    _fileRevisionPopup = am.createActionPopupMenu(ActionPlaces.UNKNOWN,fileRevisionActions);
    DefaultActionGroup branchActions = createBranchActions();
    _branchRevisionPopup = am.createActionPopupMenu(ActionPlaces.UNKNOWN,branchActions);
    DefaultActionGroup viewActions = createViewActions();
    _viewPopup = am.createActionPopupMenu(ActionPlaces.UNKNOWN,viewActions);
    return(actionToolbar.getComponent());
  }

  protected DefaultActionGroup createToolBarActions()
  {
    _ca = new CompareAction();
    _ra = new RefreshAction();
    _gra = new GetRevisionAction();
    _zia = new ZoomInAction();
    _zoa = new ZoomOutAction();
    _zda = new ZoomDefaultAction();
    _dfa = new DateFilterAction();
    _hsba = new HideSelectedBranchAction();
    _saba = new ShowAllBranchesAction();
    DefaultActionGroup retVal = new DefaultActionGroup();
    retVal.add(_ra);
    retVal.add(_zia);
    retVal.add(_zoa);
    retVal.add(_zda);
    retVal.addSeparator();
    retVal.add(_ca);
    retVal.addSeparator();
    retVal.add(_gra);
    retVal.addSeparator();
    retVal.add(_hsba);
    retVal.add(_saba);
    retVal.addSeparator();
    retVal.add(_dfa);
    return(retVal);
  }

  protected DefaultActionGroup createFileRevisionActions()
  {
    DefaultActionGroup retVal = new DefaultActionGroup();
    retVal.add(_gra);
    return(retVal);
  }

  protected DefaultActionGroup createBranchActions()
  {
    DefaultActionGroup retVal = new DefaultActionGroup();
    retVal.add(_hsba);
    return(retVal);
  }

  protected DefaultActionGroup createViewActions()
  {
    DefaultActionGroup retVal = new DefaultActionGroup();
    retVal.add(_ra);
    retVal.addSeparator();
    retVal.add(_zia);
    retVal.add(_zoa);
    retVal.add(_zda);
    retVal.addSeparator();
    retVal.add(_saba);
    retVal.addSeparator();
    retVal.add(_dfa);
    return(retVal);
  }

  @Nullable
  protected JComponent createSouthPanel()
  {
    double tableSizes[][] = {{TableLayout.FILL,TableLayout.PREFERRED,TableLayout.FILL,TableLayout.PREFERRED},{TableLayout.PREFERRED}};
    TableLayout tl = new TableLayout(tableSizes);
    tl.setHGap(5);
    tl.setVGap(5);
    JPanel retVal = new JPanel(tl);
    retVal.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
    double tableSizes2[][] = {{TableLayout.PREFERRED,TableLayout.FILL},{TableLayout.PREFERRED,TableLayout.PREFERRED,TableLayout.PREFERRED}};
    TableLayout tl2 = new TableLayout(tableSizes2);
    tl2.setHGap(5);
    tl2.setVGap(5);
    JPanel innerPanel = new JPanel(tl2);
    innerPanel.add(new JLabel("Revision:"),"0,0,r,f");
    innerPanel.add(new JLabel("Author:"),"0,1,r,f");
    innerPanel.add(new JLabel("Date:"),"0,2,r,f");
    _revisionTF = new JTextField();
    _revisionTF.setEditable(false);
    _revisionTF.setBorder(BorderFactory.createLineBorder(Color.black,1));
    innerPanel.add(_revisionTF,"1,0");
    _authorTF = new JTextField();
    _authorTF.setEditable(false);
    _authorTF.setBorder(BorderFactory.createLineBorder(Color.black,1));
    innerPanel.add(_authorTF,"1,1");
    _dateTF = new JTextField();
    _dateTF.setEditable(false);
    _dateTF.setBorder(BorderFactory.createLineBorder(Color.black,1));
    innerPanel.add(_dateTF,"1,2");
    retVal.add(innerPanel,"0,0");
    retVal.add(new JLabel("Message:"),"1,0,f,t");
    _messageTA = new JTextArea();
    _messageTA.setEditable(false);
    JScrollPane sp = new JScrollPane(_messageTA);
    retVal.add(sp,"2,0");
    JComponent buttonPanel = super.createSouthPanel();
    retVal.add(buttonPanel,"3,0,r,b");
    return(retVal);
  }

  protected void createGraph()
  {
    filterRevisions();
    _cellMap = new HashMap<String,DefaultGraphCell>();
    DefaultGraphCell lastCell = null;
    int i = 0;
    for(VcsBranch branch : _filteredRevMap.keySet()) {
      ArrayList<VcsRevisionGraphRevision> revsOnBranch = _filteredRevMap.get(branch);
      CONNECT_TYPE connectType = CONNECT_TYPE.YES;
      if(i != 0) {
        lastCell = getBranchPointCell(branch);
        if(lastCell == null) {
          connectType = CONNECT_TYPE.HALF;
          lastCell = getAncestorCell(branch);
        }
      }
      lastCell = createCell(lastCell,branch,connectType);
      for(VcsRevisionGraphRevision rev : revsOnBranch)
        lastCell = createCell(lastCell,rev,CONNECT_TYPE.YES);
      i++;
    }
    //Draw red merge arrows
    for(DefaultGraphCell cell : _cellMap.values()) {
      Object userObj = cell.getUserObject();
      if(userObj instanceof VcsBranch)
        continue;
      VcsRevisionGraphRevision rev = (VcsRevisionGraphRevision)userObj;
      String message = rev.getCommitMessage();
      if((!message.contains("Merge From: ")) || (!message.contains("Revision: ")))
        continue;
      int index = message.indexOf("Revision: ");
      //Add length of Revision:
      index += 10;
      StringBuffer revision = new StringBuffer();
      char ch = message.charAt(index);
      while((Character.isDigit(ch)) || (ch == '.')) {
        revision.append(ch);
        index++;
        if(index >= message.length())
          break;
        ch = message.charAt(index);
      }
      DefaultGraphCell mergeFromCell = _cellMap.get(revision.toString());
      if(mergeFromCell == null)
        continue;
      createEdge(mergeFromCell,cell,false,Color.red);
    }
  }

  protected String getBranchIdFromCell(DefaultGraphCell cell)
  {
    String retVal = null;

    Object userObj = cell.getUserObject();
    if(userObj instanceof VcsBranch) {
      VcsBranch branch = (VcsBranch)userObj;
      retVal = branch.getId();
    } else if(userObj instanceof VcsRevisionGraphRevision) {
      VcsRevisionGraphRevision rev = (VcsRevisionGraphRevision)userObj;
      retVal = rev.getParentBranchId();
    }
    return(retVal);
  }

  protected String getAncestorBranchId(HashMap<String,List<DefaultGraphCell>> branchCells,VcsBranch branch)
  {
    if(branch == null)
      return(null);
    String parentBranchId = branch.getParentBranchId();
    if(parentBranchId == null)
      return(null);
    if(branchCells.containsKey(parentBranchId))
      return(parentBranchId);
    else {
      VcsBranch parentBranch = _branchMap.get(parentBranchId);
      return(getAncestorBranchId(branchCells,parentBranch));
    }
  }

  protected DefaultGraphCell getBranchPointCell(VcsBranch branch)
  {
    return(_cellMap.get(branch.getParentBranchPointRevision()));
  }

  protected DefaultGraphCell getAncestorCell(VcsBranch branch)
  {
    String branchPointRevision = branch.getParentBranchPointRevision();
    if(branchPointRevision == null)
      return(null);
    DefaultGraphCell retVal = _cellMap.get(branchPointRevision);
    if(retVal == null) {
      String parentBranchId = branch.getParentBranchId();
      if(parentBranchId == null)
        return(null);
      VcsBranch parentBranch = _branchMap.get(parentBranchId);
      if(parentBranch == null)
        return(null);
      retVal = _cellMap.get(parentBranch.getId());
      if(retVal == null)
        retVal = getAncestorCell(parentBranch);
    }
    return(retVal);
  }

  protected void layoutGraph()
  {
    DefaultGraphCell lastCell = null;
    Double nextX = null;
    int i = 0;
    for(VcsBranch branch : _filteredRevMap.keySet()) {
      ArrayList<VcsRevisionGraphRevision> revsOnBranch = _filteredRevMap.get(branch);
      if(i != 0)
        lastCell = getAncestorCell(branch);
      lastCell = positionCell(lastCell,SwingConstants.RIGHT,nextX,branch);
      Rectangle2D rect = GraphConstants.getBounds(lastCell.getAttributes());
      Double widestX = rect.getX() + rect.getWidth();
      for(VcsRevisionGraphRevision rev : revsOnBranch) {
        lastCell = positionCell(lastCell,SwingConstants.BOTTOM,nextX,rev);
        rect = GraphConstants.getBounds(lastCell.getAttributes());
        if(widestX == null)
          widestX = rect.getX() + rect.getWidth();
        else {
          double wideX = rect.getX() + rect.getWidth();
          if(widestX < wideX)
            widestX = wideX;
        }
      }
      nextX = widestX + CELL_SPACING;
      i++;
    }
    //Now adjust y position in order of date
    //Sort revision list by date for use later in layoutGraph
    Collections.sort(_revisions,_revDateCompare);
    lastCell = null;
    for(i = 0; i < _revisions.size(); i++) {
      VcsRevisionGraphRevision rev = _revisions.get(i);
      //Skip first revision since everything will be based on it
      if(lastCell == null) {
        lastCell = _cellMap.get(rev.getRevisionNumber().asString());
        continue;
      }
      DefaultGraphCell cell = _cellMap.get(rev.getRevisionNumber().asString());
      if(cell == null)
        continue;
      Rectangle2D rect = GraphConstants.getBounds(cell.getAttributes());
      lastCell = positionCell(lastCell,SwingConstants.BOTTOM,rect.getX(),rev);
    }
    //Now fix branch header cells since branch point revisions may have been moved down
    i = 0;
    for(VcsBranch branch : _filteredRevMap.keySet()) {
      if(i == 0) {
        i++;
        continue;
      }
      ArrayList<VcsRevisionGraphRevision> revsOnBranch = _filteredRevMap.get(branch);
      //Use branch point rev as placement guide
      DefaultGraphCell parentCell = getAncestorCell(branch);
      DefaultGraphCell branchCell = _cellMap.get(branch.getId());
      Rectangle2D branchRect = GraphConstants.getBounds(branchCell.getAttributes());
      positionCell(parentCell,SwingConstants.RIGHT,branchRect.getX(),branch);
      branchRect = GraphConstants.getBounds(branchCell.getAttributes());
      //Check for overlap with first rev in branch
      if(revsOnBranch.size() != 0) {
        VcsRevisionGraphRevision firstRev = revsOnBranch.get(0);
        DefaultGraphCell firstCell = _cellMap.get(firstRev.getRevisionNumber().asString());
        Rectangle2D firstRect = GraphConstants.getBounds(firstCell.getAttributes());
        if((firstRect.getY() - CELL_SPACING) < (branchRect.getY() + branchRect.getHeight()))
          positionCell(firstCell,SwingConstants.TOP,null,branch);
      }
      i++;
    }
    //Now calculate visual extents of each branch
    HashMap<String,List<DefaultGraphCell>> branchCells = new HashMap<String,List<DefaultGraphCell>>();
    HashMap<String,Rectangle2D> branchBounds = new HashMap<String,Rectangle2D>();
    for(DefaultGraphCell cell : _cellMap.values()) {
      Rectangle2D rect = GraphConstants.getBounds(cell.getAttributes());
      String branchId = getBranchIdFromCell(cell);
      List<DefaultGraphCell> cells = branchCells.get(branchId);
      if(cells == null) {
        cells = new ArrayList<DefaultGraphCell>();
        branchCells.put(branchId,cells);
      }
      cells.add(cell);
      Rectangle2D branchRect = branchBounds.get(branchId);
      if(branchRect == null) {
        branchRect = new Rectangle2D.Double(rect.getX(),rect.getY(),rect.getWidth(),rect.getHeight());
        branchBounds.put(branchId,branchRect);
      } else
        Rectangle2D.union(branchRect,rect,branchRect);
    }
    //Sort branches
    //Use branch order from revMap since it is already sorted
    ArrayList<VcsBranch> branchRevisions = new ArrayList<VcsBranch>(_filteredRevMap.keySet());
    //Now optimize width of graph by moving non-overlapping branches closer together
    //Start at 3 branch since the first two will never need to be moved closer
    for(i = 2; i < branchRevisions.size(); i++) {
      VcsBranch branch = branchRevisions.get(i);
      String branchId = branch.getId();
      //Find closest ancestor
      String ancestorRevision = getAncestorBranchId(branchCells,branch);
      if(ancestorRevision == null)
        continue;
      Rectangle2D ancestorRect = branchBounds.get(ancestorRevision);
      Double suggestX = moveBranch(branchId,branchBounds,branchCells,ancestorRect.getX() + ancestorRect.getWidth() + CELL_SPACING);
      while(suggestX != null)
        suggestX = moveBranch(branchId,branchBounds,branchCells,suggestX);
    }
  }

  public void setCurrentRevision(String currentRevision)
  {
    _cellViewFactory.setCurrentRevision(currentRevision);
    DefaultGraphCell cell = _cellMap.get(currentRevision);
    _g.setSelectionCell(cell);
    _g.scrollCellToVisible(cell);
  }

  public void compare()
  {
    Object selCells[] = _g.getSelectionCells();
    if((selCells == null) || (selCells.length != 2))
      return;
    DefaultGraphCell selCell1 = (DefaultGraphCell)selCells[0];
    DefaultGraphCell selCell2 = (DefaultGraphCell)selCells[1];
    VcsRevisionGraphRevision selRev1 = (VcsRevisionGraphRevision)selCell1.getUserObject();
    VcsRevisionGraphRevision selRev2 = (VcsRevisionGraphRevision)selCell2.getUserObject();
    compareFileRevisions(selRev1,selRev2);
  }

  protected void compareFileRevisions(VcsRevisionGraphRevision rev1,VcsRevisionGraphRevision rev2)
  {
    if(_revCompare.compare(rev1,rev2) > 0) {
      VcsRevisionGraphRevision tempRev = rev2;
      rev2 = rev1;
      rev1 = tempRev;
    }
    VcsVirtualFile selFile1 = new VcsVirtualFile(_filePath.getPath(),rev1, VcsFileSystem.getInstance());
    VcsVirtualFile selFile2 = new VcsVirtualFile(_filePath.getPath(),rev2,VcsFileSystem.getInstance());
    DiffTool dt = DiffManager.getInstance().getDiffTool();
    dt.show(SimpleDiffRequest.compareFiles(selFile1,selFile2,_project,_filePath.getName() + ": " + rev1.getRevisionNumber().asString() + "->" + rev2.getRevisionNumber().asString()));
  }

  public void refresh(boolean readRevisions)
  {
    GraphLayoutCache glc = _g.getGraphLayoutCache();
    glc.remove(glc.getCells(glc.getAllViews()));
    _cellMap = null;
    String currentRev = null;
    if(readRevisions)
      currentRev = getAllRevisions();
    createGraph();
    layoutGraph();
    if(readRevisions)
      setCurrentRevision(currentRev);
  }

  protected void oneFileRevisionCellSelectedRequired(Presentation p)
  {
    if(_g == null) {
      p.setEnabled(false);
      return;
    }
    Object selCells[] = _g.getSelectionCells();
    if((selCells != null) && (selCells.length == 1)) {
      if(((DefaultGraphCell)selCells[0]).getUserObject() instanceof VcsRevisionGraphRevision)
        p.setEnabled(true);
      else
        p.setEnabled(false);
    } else
      p.setEnabled(false);
  }

  protected void oneBranchCellSelectedRequired(Presentation p)
  {
    if(_g == null) {
      p.setEnabled(false);
      return;
    }
    Object selCells[] = _g.getSelectionCells();
    if((selCells != null) && (selCells.length == 1)) {
      if(((DefaultGraphCell)selCells[0]).getUserObject() instanceof VcsBranch)
        p.setEnabled(true);
      else
        p.setEnabled(false);
    } else
      p.setEnabled(false);
  }

  protected void twoFileRevisionCellsSelectedRequired(Presentation p)
  {
    if(_g == null) {
      p.setEnabled(false);
      return;
    }
    Object selCells[] = _g.getSelectionCells();
    if((selCells != null) && (selCells.length == 2)) {
      if((((DefaultGraphCell)selCells[0]).getUserObject() instanceof VcsRevisionGraphRevision) && (((DefaultGraphCell)selCells[1]).getUserObject() instanceof VcsRevisionGraphRevision))
        p.setEnabled(true);
      else
        p.setEnabled(false);
    } else
      p.setEnabled(false);
  }

  public void zoomIn()
  {
    _g.setScale(_g.getScale() + 0.1);
  }

  public void zoomOut()
  {
    double scale = _g.getScale();
    if(scale <= 0.1)
      return;
    _g.setScale(scale - 0.1);
   }

  public void zoomDefault()
  {
    _g.setScale(1.0);
   }

  public void getRevision()
  {
    Object selCells[] = _g.getSelectionCells();
    if((selCells == null) || (selCells.length != 1))
      return;
    DefaultGraphCell selCell = (DefaultGraphCell)selCells[0];
    VcsRevisionGraphRevision selRev = (VcsRevisionGraphRevision)selCell.getUserObject();
    try {
      Editor editor = FileEditorManager.getInstance(_project).getSelectedTextEditor();
      //noinspection ConstantConditions
      final Document doc = editor.getDocument();
      //Check not modified
      ReplaceFileConfirmationDialog confirmDialog = new ReplaceFileConfirmationDialog(_project,"Get Selected Revision");
      VirtualFile confirmFiles[] = new VirtualFile[1];
      VirtualFile selFile = FileDocumentManager.getInstance().getFile(doc);
      confirmFiles[0] = selFile;
      if(!confirmDialog.confirmFor(confirmFiles))
        return;
      selRev.loadContent();
      byte content[] = selRev.getContent();
      final String contentStr = new String(content);
      ApplicationManager.getApplication().runWriteAction(new Runnable() {
        public void run()
        {
          doc.setText(contentStr);
          FileDocumentManager.getInstance().saveDocument(doc);
        }
      });
    } catch(Throwable t) {
      Messages.showErrorDialog(_project,"Cannot get selected revision","CVS Error");
    }
  }

  public void hideSelectedBranch()
  {
    Object selCells[] = _g.getSelectionCells();
    if((selCells == null) || (selCells.length != 1))
      return;
    DefaultGraphCell selCell = (DefaultGraphCell)selCells[0];
    VcsBranch selRev = (VcsBranch)selCell.getUserObject();
    if(_showBranchFilter)
      _branchFilter.remove(selRev.getName());
    else
      _branchFilter.add(selRev.getName());
    refresh(false);
  }

  public void showAllBranches()
  {
    if(_showBranchFilter)
      _showBranchFilter = false;
    _branchFilter.clear();
    refresh(false);
  }

  public void dateFilter()
  {
    DateFilterOptions dialog = new DateFilterOptions(_project);
    dialog.show(_showRevisionFilter,_afterDateTimeFilter,_beforeDateTimeFilter,_afterDateTime,_beforeDateTime);
    DateFilterOptions.DateFilteringOptions options = dialog.getReturnValue();
    if(options == null)
      return;
    _showRevisionFilter = options._showRevisionFilter;
    _afterDateTimeFilter = options._afterDateTimeFilter;
    _beforeDateTimeFilter = options._beforeDateTimeFilter;
    _afterDateTime = options._afterDateTime;
    _beforeDateTime = options._beforeDateTime;
    refresh(false);
  }

  protected DefaultGraphCell createCell(DefaultGraphCell neighborCell,Object userObject,CONNECT_TYPE connected)
  {
    DefaultGraphCell retVal = new DefaultGraphCell(userObject);
    if(userObject instanceof VcsRevisionGraphRevision) {
      VcsRevisionGraphRevision rev = (VcsRevisionGraphRevision)userObject;
      _cellMap.put(rev.getRevisionNumber().asString(),retVal);
    } else if(userObject instanceof VcsBranch) {
      VcsBranch bRev = (VcsBranch)userObject;
      _cellMap.put(bRev.getId(),retVal);
    }
    GraphConstants.setOpaque(retVal.getAttributes(),true);
    _g.getGraphLayoutCache().insert(retVal);
    if((neighborCell != null) && (connected != CONNECT_TYPE.NO))
      createEdge(neighborCell,retVal,connected == CONNECT_TYPE.HALF,Color.black);
    return(retVal);
  }

  protected DefaultEdge createEdge(DefaultGraphCell source,DefaultGraphCell target,boolean dashed,Color color)
  {
    DefaultPort sourceP = new DefaultPort();
    source.add(sourceP);
    DefaultPort targetP = new DefaultPort();
    target.add(targetP);
    DefaultEdge retVal = new DefaultEdge();
    retVal.setSource(sourceP);
    retVal.setTarget(targetP);
    GraphConstants.setLineEnd(retVal.getAttributes(),GraphConstants.ARROW_CLASSIC);
    GraphConstants.setEndFill(retVal.getAttributes(),true);
    GraphConstants.setLineColor(retVal.getAttributes(),color);
    if(dashed)
      GraphConstants.setDashPattern(retVal.getAttributes(),DASH_PATTERN);
    _g.getGraphLayoutCache().insert(retVal);
    return(retVal);
  }

  //Returns null if move successful, otherwise, returns suggested X location to try again
  protected Double moveBranch(String branch,HashMap<String, Rectangle2D> branchBounds,HashMap<String,List<DefaultGraphCell>> branchCellMap,double x)
  {
    Rectangle2D oldBranchBounds = branchBounds.get(branch);
    Rectangle2D.Double newBranchBounds = new Rectangle2D.Double(x,oldBranchBounds.getY(),oldBranchBounds.getWidth(),oldBranchBounds.getHeight());
    if(newBranchBounds.equals(oldBranchBounds))
      return(null);
    //Check for collisions
    for(Map.Entry<String,Rectangle2D> entry : branchBounds.entrySet()) {
      String candidateBranchRevision = entry.getKey();
      Rectangle2D candidateRect = entry.getValue();
      //Skip myself
      if(candidateBranchRevision.equals(branch))
        continue;
      if(newBranchBounds.intersects(candidateRect))
        return(candidateRect.getX() + candidateRect.getWidth() + CELL_SPACING);
    }
    branchBounds.put(branch,newBranchBounds);
    List<DefaultGraphCell> branchCellList = branchCellMap.get(branch);
    for(DefaultGraphCell cell : branchCellList) {
      Rectangle2D rect = GraphConstants.getBounds(cell.getAttributes());
      rect = new Rectangle2D.Double(x,rect.getY(),rect.getWidth(),rect.getHeight());
      GraphConstants.setBounds(cell.getAttributes(),rect);
      _g.getGraphLayoutCache().editCell(cell,cell.getAttributes());
    }
    return(null);
  }

  protected DefaultGraphCell positionCell(DefaultGraphCell neighborCell,int relativePosition,Double preferredX,Object userObject)
  {
    DefaultGraphCell retVal = null;
    if(userObject instanceof VcsRevisionGraphRevision) {
      VcsRevisionGraphRevision rev = (VcsRevisionGraphRevision)userObject;
      retVal = _cellMap.get(rev.getRevisionNumber().asString());
    } else if(userObject instanceof VcsBranch) {
      VcsBranch bRev = (VcsBranch)userObject;
      retVal = _cellMap.get(bRev.getId());
    }
    //noinspection ConstantConditions
    Rectangle2D userRect = GraphConstants.getBounds(retVal.getAttributes());
    //Calculate position from neighbor
    Point2D.Double p = null;
    Rectangle2D neighborRect = null;
    if(neighborCell == null) {
      if(relativePosition == SwingConstants.BOTTOM)
        neighborRect = new Rectangle2D.Double(CELL_SPACING,0,0,0);
      else if(relativePosition == SwingConstants.RIGHT)
        neighborRect = new Rectangle2D.Double(0,CELL_SPACING,0,0);
    } else
      neighborRect = GraphConstants.getBounds(neighborCell.getAttributes());
    if(relativePosition == SwingConstants.BOTTOM) {
      double x = neighborRect.getX();
      double y = neighborRect.getY() + neighborRect.getHeight() + CELL_SPACING;
      p = new Point2D.Double(x,y);
    } else if(relativePosition == SwingConstants.RIGHT) {
      double x = neighborRect.getX() + neighborRect.getWidth() + CELL_SPACING;
      double y = neighborRect.getY();
      p = new Point2D.Double(x,y);
    } else if(relativePosition == SwingConstants.TOP) {
      //noinspection ConstantConditions
      double x = neighborRect.getX();
      double y = neighborRect.getY() - userRect.getHeight() - CELL_SPACING;
      p = new Point2D.Double(x,y);
    }
    if(preferredX != null)
      //noinspection ConstantConditions
      p.setLocation(preferredX.doubleValue(),p.getY());
    CellView cv = _g.getGraphLayoutCache().getMapping(retVal,true);
    Component comp = cv.getRendererComponent(_g,false,false,false);
    Dimension prefSize = comp.getPreferredSize();
    //noinspection ConstantConditions
    neighborRect = new Rectangle2D.Double(p.getX(),p.getY(),prefSize.getWidth(),prefSize.getHeight());
    GraphConstants.setBounds(retVal.getAttributes(),neighborRect);
    _g.getGraphLayoutCache().editCell(retVal,retVal.getAttributes());
    return(retVal);
  }

  public void dispose()
  {
    _g.removeGraphSelectionListener(this);
    _revMap = null;
    _filteredRevMap = null;
    _branchMap = null;
    _cellMap = null;
    GraphLayoutCache glc = _g.getGraphLayoutCache();
    glc.remove(glc.getCells(glc.getAllViews()));
    _g.setModel(new DefaultGraphModel());
    glc.setFactory(null);
    _cellViewFactory = null;
    _g = null;
    _project = null;
    _filePath = null;
    _selRevCell = null;
    super.dispose();
  }

  public void valueChanged(GraphSelectionEvent gse)
  {
    Object selCells[] = _g.getSelectionCells();
    Object selectedRev1 = null;
    Object selectedRev2 = null;
    if((selCells != null) && (selCells.length == 1)) {
      if(selCells[0] instanceof Edge) {
        Edge edge = (Edge)selCells[0];
        DefaultPort port1 = (DefaultPort)edge.getSource();
        DefaultGraphCell cell1 = (DefaultGraphCell)port1.getParent();
        Object user1 = cell1.getUserObject();
        DefaultPort port2 = (DefaultPort)edge.getTarget();
        DefaultGraphCell cell2 = (DefaultGraphCell)port2.getParent();
        Object user2 = cell2.getUserObject();
        if((user1 instanceof VcsRevisionGraphRevision) && (user2 instanceof VcsRevisionGraphRevision)) {
          selectedRev1 = user1;
          selectedRev2 = user2;
        }
      } else if(selCells[0] instanceof GraphCell) {
        selectedRev1 = ((DefaultGraphCell)selCells[0]).getUserObject();
        if(selectedRev1 instanceof VcsBranch)
          selectedRev1 = null;
      }
    }
    _selRevCell = null;
    if((selectedRev1 == null) || (selectedRev2 != null)) {
      _revisionTF.setText(null);
      _authorTF.setText(null);
      _dateTF.setText(null);
      _messageTA.setText(null);
    } else {
      VcsRevisionGraphRevision rev = (VcsRevisionGraphRevision)selectedRev1;
      _revisionTF.setText(rev.getRevisionNumber().asString());
      _authorTF.setText(rev.getAuthor());
      Date d = rev.getRevisionDate();
      if(d != null)
        _dateTF.setText(_dateTimeFormat.format(d));
      _messageTA.setText(rev.getCommitMessage());
      _selRevCell = rev;
    }
    if(selectedRev2 != null) {
      VcsRevisionGraphRevision rev1 = (VcsRevisionGraphRevision)selectedRev1;
      VcsRevisionGraphRevision rev2 = (VcsRevisionGraphRevision)selectedRev2;
      compareFileRevisions(rev1,rev2);
    }
  }

  protected class PopupListener extends MouseAdapter {
    protected void maybeShowPopup(MouseEvent me)
    {
      if(me.isPopupTrigger()) {
        DefaultGraphCell cell = (DefaultGraphCell)_g.getFirstCellForLocation(me.getX(),me.getY());
        if(cell == null)
          _viewPopup.getComponent().show(me.getComponent(),me.getX(),me.getY());
        else {
          Object user = cell.getUserObject();
          if(user instanceof VcsRevisionGraphRevision)
            _fileRevisionPopup.getComponent().show(me.getComponent(),me.getX(),me.getY());
          else if(user instanceof VcsBranch)
            _branchRevisionPopup.getComponent().show(me.getComponent(),me.getX(),me.getY());
        }
      }
    }

    public void mousePressed(MouseEvent me)
    {
      maybeShowPopup(me);
    }

    public void mouseReleased(MouseEvent me)
    {
      maybeShowPopup(me);
    }
  }

  protected class CompareAction extends AnAction {
    public CompareAction()
    {
      super("Compare Revisions","Compare Revisions", IconLoader.getIcon("/com/castrianni/vcs/images/compare.png"));
    }

    public void actionPerformed(AnActionEvent ae)
    {
      compare();
    }

    public void update(AnActionEvent ae)
    {
      twoFileRevisionCellsSelectedRequired(ae.getPresentation());
    }
  }

  protected class RefreshAction extends AnAction {
    public RefreshAction()
    {
      super("Refresh Graph","Refresh Graph",IconLoader.getIcon("/com/castrianni/vcs/images/refresh.png"));
    }

    public void actionPerformed(AnActionEvent ae)
    {
      refresh(true);
    }
  }

  protected class ZoomInAction extends AnAction {
    public ZoomInAction()
    {
      super("Zoom In","Zoom In",IconLoader.getIcon("/com/castrianni/vcs/images/zoomIn.png"));
    }

    public void actionPerformed(AnActionEvent ae)
    {
      zoomIn();
    }
  }

  protected class ZoomOutAction extends AnAction {
    public ZoomOutAction()
    {
      super("Zoom Out","Zoom Out",IconLoader.getIcon("/com/castrianni/vcs/images/zoomOut.png"));
    }

    public void actionPerformed(AnActionEvent ae)
    {
      zoomOut();
    }
  }

  protected class ZoomDefaultAction extends AnAction {
    public ZoomDefaultAction()
    {
      super("Zoom Default","Zoom Default",IconLoader.getIcon("/com/castrianni/vcs/images/zoomDefault.png"));
    }

    public void actionPerformed(AnActionEvent ae)
    {
      zoomDefault();
    }
  }

  protected class GetRevisionAction extends AnAction {
    public GetRevisionAction()
    {
      super("Get Selected Revision as Current","Get Selected Revision as Current",IconLoader.getIcon("/com/castrianni/vcs/images/get.png"));
    }

    public void actionPerformed(AnActionEvent ae)
    {
      getRevision();
    }

    public void update(AnActionEvent ae)
    {
      oneFileRevisionCellSelectedRequired(ae.getPresentation());
    }
  }

  protected class HideSelectedBranchAction extends AnAction {
    public HideSelectedBranchAction()
    {
      super("Hide Selected VcsBranch","Hide Selected VcsBranch",IconLoader.getIcon("/com/castrianni/vcs/images/hideBranch.png"));
    }

    public void actionPerformed(AnActionEvent ae)
    {
      hideSelectedBranch();
    }

    public void update(AnActionEvent ae)
    {
      oneBranchCellSelectedRequired(ae.getPresentation());
    }
  }

  protected class ShowAllBranchesAction extends AnAction {
    public ShowAllBranchesAction()
    {
      super("Show All Branches","Show All Branches",IconLoader.getIcon("/com/castrianni/vcs/images/showAllBranches.png"));
    }

    public void actionPerformed(AnActionEvent ae)
    {
      showAllBranches();
    }

    public void update(AnActionEvent ae)
    {
      Presentation p = ae.getPresentation();
      if(!_showBranchFilter)
        p.setEnabled(!_branchFilter.isEmpty());
      else
        p.setEnabled(true);
    }
  }

  protected class DateFilterAction extends AnAction {
    public DateFilterAction()
    {
      super("Set Date Filter","Set Date Filter",IconLoader.getIcon("/com/castrianni/vcs/images/dateFilter.png"));
    }

    public void actionPerformed(AnActionEvent ae)
    {
      dateFilter();
    }
  }
}
